/*
 * Zed Attack Proxy (ZAP) and its related class files.
 *
 * ZAP is an HTTP/HTTPS proxy for assessing web application security.
 *
 * Copyright 2013 The ZAP Development Team
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.zaproxy.zap.extension.api;

import java.io.File;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.function.Consumer;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import net.sf.json.JSONArray;
import net.sf.json.JSONException;
import net.sf.json.JSONObject;
import org.apache.commons.httpclient.URI;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.parosproxy.paros.Constant;
import org.parosproxy.paros.model.Model;
import org.parosproxy.paros.model.SiteNode;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Text;
import org.zaproxy.zap.authentication.AuthenticationMethod;
import org.zaproxy.zap.authentication.AuthenticationMethod.AuthCheckingStrategy;
import org.zaproxy.zap.authentication.AuthenticationMethod.AuthPollFrequencyUnits;
import org.zaproxy.zap.authentication.AuthenticationMethodType;
import org.zaproxy.zap.extension.api.ApiException.Type;
import org.zaproxy.zap.extension.authorization.AuthorizationDetectionMethod;
import org.zaproxy.zap.model.Context;
import org.zaproxy.zap.model.IllegalContextNameException;
import org.zaproxy.zap.model.Tech;
import org.zaproxy.zap.model.TechSet;
import org.zaproxy.zap.utils.ApiUtils;
import org.zaproxy.zap.utils.JsonUtil;
import org.zaproxy.zap.utils.XMLStringUtil;

public class ContextAPI extends ApiImplementor {

    private static final Logger LOGGER = LogManager.getLogger(ContextAPI.class);

    private static final String PREFIX = "context";
    private static final String TECH_NAME = "technologyName";
    private static final String ACTION_EXCLUDE_FROM_CONTEXT_REGEX = "excludeFromContext";
    private static final String ACTION_INCLUDE_IN_CONTEXT_REGEX = "includeInContext";
    private static final String ACTION_SET_CONTEXT_REGEXS = "setContextRegexs";
    private static final String ACTION_SET_CONTEXT_CHECKING_STRATEGY = "setContextCheckingStrategy";
    private static final String ACTION_NEW_CONTEXT = "newContext";
    private static final String ACTION_REMOVE_CONTEXT = "removeContext";
    private static final String ACTION_SET_CONTEXT_IN_SCOPE = "setContextInScope";
    private static final String ACTION_EXPORT_CONTEXT = "exportContext";
    private static final String ACTION_IMPORT_CONTEXT = "importContext";
    private static final String ACTION_INCLUDE_TECHS = "includeContextTechnologies";
    private static final String ACTION_INCLUDE_ALL_TECHS = "includeAllContextTechnologies";
    private static final String ACTION_EXCLUDE_TECHS = "excludeContextTechnologies";
    private static final String ACTION_EXCLUDE_ALL_TECHS = "excludeAllContextTechnologies";
    private static final String VIEW_EXCLUDE_REGEXS = "excludeRegexs";
    private static final String VIEW_INCLUDE_REGEXS = "includeRegexs";
    private static final String VIEW_CONTEXT_LIST = "contextList";
    private static final String VIEW_CONTEXT = "context";
    private static final String VIEW_ALL_TECHS = "technologyList";
    private static final String VIEW_INCLUDED_TECHS = "includedTechnologyList";
    private static final String VIEW_EXCLUDED_TECHS = "excludedTechnologyList";
    private static final String VIEW_URLS = "urls";
    private static final String REGEX_PARAM = "regex";
    private static final String INC_REGEXS_PARAM = "incRegexs";
    private static final String EXC_REGEXS_PARAM = "excRegexs";
    private static final String CONTEXT_NAME = "contextName";
    private static final String IN_SCOPE = "booleanInScope";
    private static final String CONTEXT_FILE_PARAM = "contextFile";
    private static final String CONTEXT_ID = "contextId";
    private static final String PARAM_TECH_NAMES = "technologyNames";
    private static final String PARAM_CHECKING_STRATEGRY = "checkingStrategy";
    private static final String PARAM_POLL_URL = "pollUrl";
    private static final String PARAM_POLL_DATA = "pollData";
    private static final String PARAM_POLL_HEADERS = "pollHeaders";
    private static final String PARAM_POLL_FREQ = "pollFrequency";
    private static final String PARAM_POLL_FREQ_UNITS = "pollFrequencyUnits";

    public ContextAPI() {
        List<String> contextNameAndRegexParam = new ArrayList<>(2);
        contextNameAndRegexParam.add(CONTEXT_NAME);
        contextNameAndRegexParam.add(REGEX_PARAM);
        List<String> contextNameOnlyParam = new ArrayList<>(1);
        contextNameOnlyParam.add((CONTEXT_NAME));
        String[] contextNameAndTechNames = new String[] {CONTEXT_NAME, PARAM_TECH_NAMES};

        this.addApiAction(
                new ApiAction(ACTION_EXCLUDE_FROM_CONTEXT_REGEX, contextNameAndRegexParam));
        this.addApiAction(new ApiAction(ACTION_INCLUDE_IN_CONTEXT_REGEX, contextNameAndRegexParam));
        this.addApiAction(
                new ApiAction(
                        ACTION_SET_CONTEXT_REGEXS,
                        new String[] {CONTEXT_NAME, INC_REGEXS_PARAM, EXC_REGEXS_PARAM}));
        this.addApiAction(
                new ApiAction(
                        ACTION_SET_CONTEXT_CHECKING_STRATEGY,
                        new String[] {CONTEXT_NAME, PARAM_CHECKING_STRATEGRY},
                        new String[] {
                            PARAM_POLL_URL,
                            PARAM_POLL_DATA,
                            PARAM_POLL_HEADERS,
                            PARAM_POLL_FREQ,
                            PARAM_POLL_FREQ_UNITS
                        }));
        this.addApiAction(new ApiAction(ACTION_NEW_CONTEXT, contextNameOnlyParam));
        this.addApiAction(new ApiAction(ACTION_REMOVE_CONTEXT, contextNameOnlyParam));
        this.addApiAction(
                new ApiAction(
                        ACTION_EXPORT_CONTEXT,
                        new String[] {CONTEXT_NAME, CONTEXT_FILE_PARAM},
                        null));
        this.addApiAction(
                new ApiAction(ACTION_IMPORT_CONTEXT, new String[] {CONTEXT_FILE_PARAM}, null));
        this.addApiAction(new ApiAction(ACTION_INCLUDE_TECHS, contextNameAndTechNames));
        this.addApiAction(new ApiAction(ACTION_INCLUDE_ALL_TECHS, contextNameOnlyParam));
        this.addApiAction(new ApiAction(ACTION_EXCLUDE_TECHS, contextNameAndTechNames));
        this.addApiAction(new ApiAction(ACTION_EXCLUDE_ALL_TECHS, contextNameOnlyParam));

        List<String> contextInScopeParams = new ArrayList<>(2);
        contextInScopeParams.add(CONTEXT_NAME);
        contextInScopeParams.add(IN_SCOPE);
        this.addApiAction(new ApiAction(ACTION_SET_CONTEXT_IN_SCOPE, contextInScopeParams));

        this.addApiView(new ApiView(VIEW_CONTEXT_LIST));
        this.addApiView(new ApiView(VIEW_EXCLUDE_REGEXS, contextNameOnlyParam));
        this.addApiView(new ApiView(VIEW_INCLUDE_REGEXS, contextNameOnlyParam));
        this.addApiView(new ApiView(VIEW_CONTEXT, contextNameOnlyParam));
        this.addApiView(new ApiView(VIEW_ALL_TECHS));
        this.addApiView(new ApiView(VIEW_INCLUDED_TECHS, contextNameOnlyParam));
        this.addApiView(new ApiView(VIEW_EXCLUDED_TECHS, contextNameOnlyParam));
        this.addApiView(new ApiView(VIEW_URLS, contextNameOnlyParam));
    }

    @Override
    public String getPrefix() {
        return PREFIX;
    }

    @Override
    public ApiResponse handleApiAction(String name, JSONObject params) throws ApiException {
        LOGGER.debug("handleApiAction {} {}", name, params);

        Context context;
        TechSet techSet;
        String[] techNames;
        String filename;
        File f;

        switch (name) {
            case ACTION_EXCLUDE_FROM_CONTEXT_REGEX:
                try {
                    addExcludeToContext(getContext(params), params.getString(REGEX_PARAM));
                } catch (IllegalArgumentException e) {
                    throw new ApiException(ApiException.Type.ILLEGAL_PARAMETER, REGEX_PARAM, e);
                }
                break;
            case ACTION_INCLUDE_IN_CONTEXT_REGEX:
                try {
                    addIncludeToContext(getContext(params), params.getString(REGEX_PARAM));
                } catch (IllegalArgumentException e) {
                    throw new ApiException(ApiException.Type.ILLEGAL_PARAMETER, REGEX_PARAM, e);
                }
                break;
            case ACTION_SET_CONTEXT_REGEXS:
                context = getContext(params);
                JSONArray incRegexs;
                JSONArray excRegexs;
                try {
                    incRegexs = JSONArray.fromObject(params.get(INC_REGEXS_PARAM));
                    context.setIncludeInContextRegexs(JsonUtil.toStringList(incRegexs));
                } catch (JSONException e1) {
                    throw new ApiException(ApiException.Type.ILLEGAL_PARAMETER, INC_REGEXS_PARAM);
                }
                try {
                    excRegexs = JSONArray.fromObject(params.get(EXC_REGEXS_PARAM));
                    context.setExcludeFromContextRegexs(JsonUtil.toStringList(excRegexs));
                } catch (Exception e1) {
                    throw new ApiException(ApiException.Type.ILLEGAL_PARAMETER, EXC_REGEXS_PARAM);
                }
                Model.getSingleton().getSession().saveContext(context);
                break;
            case ACTION_SET_CONTEXT_CHECKING_STRATEGY:
                context = getContext(params);
                AuthCheckingStrategy checkingStrategy;
                try {
                    checkingStrategy =
                            AuthCheckingStrategy.valueOf(
                                    params.getString(PARAM_CHECKING_STRATEGRY));
                } catch (Exception e1) {
                    throw new ApiException(
                            ApiException.Type.ILLEGAL_PARAMETER, PARAM_CHECKING_STRATEGRY);
                }
                if (AuthCheckingStrategy.POLL_URL.equals(checkingStrategy)) {
                    AuthPollFrequencyUnits units;
                    try {
                        units =
                                AuthPollFrequencyUnits.valueOf(
                                        params.getString(PARAM_POLL_FREQ_UNITS));
                    } catch (Exception e) {
                        throw new ApiException(
                                ApiException.Type.ILLEGAL_PARAMETER, PARAM_POLL_FREQ_UNITS);
                    }
                    int freq;
                    String pollUrl = params.getString(PARAM_POLL_URL);
                    String pollData = params.getString(PARAM_POLL_DATA);
                    String pollHeaders = params.getString(PARAM_POLL_HEADERS);
                    if (pollUrl == null || pollUrl.isEmpty()) {
                        throw new ApiException(ApiException.Type.ILLEGAL_PARAMETER, PARAM_POLL_URL);
                    }
                    try {
                        new URI(pollUrl, true);
                    } catch (Exception e) {
                        throw new ApiException(ApiException.Type.ILLEGAL_PARAMETER, PARAM_POLL_URL);
                    }
                    try {
                        freq = params.getInt(PARAM_POLL_FREQ);
                    } catch (Exception e) {
                        throw new ApiException(
                                ApiException.Type.ILLEGAL_PARAMETER, PARAM_POLL_FREQ);
                    }
                    if (freq <= 0) {
                        throw new ApiException(
                                ApiException.Type.ILLEGAL_PARAMETER, PARAM_POLL_FREQ);
                    }
                    context.getAuthenticationMethod().setPollUrl(pollUrl);
                    context.getAuthenticationMethod().setPollData(pollData);
                    context.getAuthenticationMethod().setPollHeaders(pollHeaders);
                    context.getAuthenticationMethod().setPollFrequency(freq);
                    context.getAuthenticationMethod().setPollFrequencyUnits(units);
                }
                context.getAuthenticationMethod().setAuthCheckingStrategy(checkingStrategy);
                Model.getSingleton().getSession().saveContext(context);
                break;
            case ACTION_NEW_CONTEXT:
                String contextName = params.getString(CONTEXT_NAME);
                try {
                    context = Model.getSingleton().getSession().getNewContext(contextName);
                } catch (IllegalContextNameException e) {
                    throw new ApiException(ApiException.Type.ALREADY_EXISTS, contextName, e);
                }
                Model.getSingleton().getSession().saveContext(context);
                return new ApiResponseElement(CONTEXT_ID, String.valueOf(context.getId()));
            case ACTION_REMOVE_CONTEXT:
                context = getContext(params);
                Model.getSingleton().getSession().deleteContext(context);
                break;
            case ACTION_SET_CONTEXT_IN_SCOPE:
                context = getContext(params);
                context.setInScope(params.getBoolean(IN_SCOPE));
                Model.getSingleton().getSession().saveContext(context);
                break;
            case ACTION_IMPORT_CONTEXT:
                filename = params.getString(CONTEXT_FILE_PARAM);
                f = new File(filename);
                if (!f.exists()) {
                    // Try relative to the contexts dir
                    f = new File(Constant.getContextsDir(), filename);
                }
                if (!f.exists()) {
                    throw new ApiException(ApiException.Type.DOES_NOT_EXIST, f.getAbsolutePath());
                } else {
                    try {
                        context = Model.getSingleton().getSession().importContext(f);
                    } catch (IllegalContextNameException e) {
                        throw new ApiException(ApiException.Type.BAD_EXTERNAL_DATA, e);
                    } catch (Exception e) {
                        LOGGER.error(e.getMessage(), e);
                        throw new ApiException(ApiException.Type.INTERNAL_ERROR, e.getMessage());
                    }
                }
                return new ApiResponseElement(CONTEXT_ID, String.valueOf(context.getId()));
            case ACTION_EXPORT_CONTEXT:
                filename = params.getString(CONTEXT_FILE_PARAM);
                context = getContext(params);

                f = new File(filename);
                if (!f.getAbsolutePath().equals(filename)) {
                    // Not an absolute filename, use one relative to the contexts dir
                    f = new File(Constant.getContextsDir(), filename);
                }
                if (!f.getParentFile().canWrite()) {
                    // Cant write to the parent dir so not looking good
                    throw new ApiException(ApiException.Type.NO_ACCESS, f.getAbsolutePath());
                } else {
                    try {
                        Model.getSingleton().getSession().exportContext(context, f);
                    } catch (Exception e) {
                        throw new ApiException(ApiException.Type.INTERNAL_ERROR, e.getMessage());
                    }
                }
                break;
            case ACTION_INCLUDE_TECHS:
                context = getContext(params);
                techSet = context.getTechSet();
                techNames = getParam(params, PARAM_TECH_NAMES, "").split(",");
                handleTechs(techNames, techSet::includeAll);
                context.save();
                break;
            case ACTION_INCLUDE_ALL_TECHS:
                context = getContext(params);
                techSet = new TechSet(Tech.getAll());
                context.setTechSet(techSet);
                context.save();
                break;
            case ACTION_EXCLUDE_TECHS:
                context = getContext(params);
                techSet = context.getTechSet();
                techNames = getParam(params, PARAM_TECH_NAMES, "").split(",");
                handleTechs(techNames, techSet::excludeAll);
                context.save();
                break;
            case ACTION_EXCLUDE_ALL_TECHS:
                context = getContext(params);
                techSet = context.getTechSet();
                for (Tech tech : Tech.getAll()) {
                    techSet.exclude(tech);
                }
                context.save();
                break;
            default:
                throw new ApiException(Type.BAD_ACTION);
        }

        return ApiResponseElement.OK;
    }

    private void handleTechs(String[] techNames, Consumer<Tech> handler) throws ApiException {
        for (String techName : techNames) {
            Tech tech = getTech(techName);
            handler.accept(tech);
        }
    }

    private void addExcludeToContext(Context context, String regex) {
        List<String> incRegexes = new ArrayList<>(context.getIncludeInContextRegexs());
        if (incRegexes.remove(regex)) {
            // Its already explicitly included, removing it from the include list is safer and more
            // useful
            context.setIncludeInContextRegexs(incRegexes);
        } else {
            context.addExcludeFromContextRegex(regex);
        }
        Model.getSingleton().getSession().saveContext(context);
    }

    private void addIncludeToContext(Context context, String regex) {
        context.addIncludeInContextRegex(regex);
        Model.getSingleton().getSession().saveContext(context);
    }

    @Override
    public ApiResponse handleApiView(String name, JSONObject params) throws ApiException {
        LOGGER.debug("handleApiView {} {}", name, params);

        ApiResponse result;
        ApiResponseList resultList;
        TechSet techSet;

        switch (name) {
            case VIEW_EXCLUDE_REGEXS:
                resultList = new ApiResponseList(name);
                for (String regex : getContext(params).getExcludeFromContextRegexs()) {
                    resultList.addItem(new ApiResponseElement(REGEX_PARAM, regex));
                }
                result = resultList;
                break;
            case VIEW_INCLUDE_REGEXS:
                resultList = new ApiResponseList(name);
                for (String regex : getContext(params).getIncludeInContextRegexs()) {
                    resultList.addItem(new ApiResponseElement(REGEX_PARAM, regex));
                }
                result = resultList;
                break;
            case VIEW_CONTEXT_LIST:
                resultList = new ApiResponseList(name);
                for (Context context : Model.getSingleton().getSession().getContexts()) {
                    resultList.addItem(new ApiResponseElement(CONTEXT_NAME, context.getName()));
                }
                result = resultList;
                break;
            case VIEW_CONTEXT:
                result = new ApiResponseElement(buildResponseFromContext(getContext(params)));
                break;
            case VIEW_ALL_TECHS:
                resultList = new ApiResponseList(name);
                for (Tech tech : Tech.getAll()) {
                    resultList.addItem(new ApiResponseElement(TECH_NAME, tech.toString()));
                }
                result = resultList;
                break;
            case VIEW_INCLUDED_TECHS:
                resultList = new ApiResponseList(name);
                techSet = getContext(params).getTechSet();
                for (Tech tech : techSet.getIncludeTech()) {
                    resultList.addItem(new ApiResponseElement(TECH_NAME, tech.toString()));
                }
                result = resultList;
                break;
            case VIEW_EXCLUDED_TECHS:
                resultList = new ApiResponseList(name);
                techSet = getContext(params).getTechSet();
                for (Tech tech : techSet.getExcludeTech()) {
                    resultList.addItem(new ApiResponseElement(TECH_NAME, tech.toString()));
                }
                result = resultList;
                break;
            case VIEW_URLS:
                resultList = new ApiResponseList(name);
                Set<String> addedUrls = new HashSet<>();
                for (SiteNode node : getContext(params).getNodesInContextFromSiteTree()) {
                    String uri = node.getHistoryReference().getURI().toString();
                    if (!addedUrls.contains(uri)) {
                        resultList.addItem(new ApiResponseElement("url", uri));
                        addedUrls.add(uri);
                    }
                }
                result = resultList;
                break;
            default:
                throw new ApiException(Type.BAD_VIEW);
        }
        return result;
    }

    /**
     * Returns the {@code Context} with the given name. The context's name is obtained from the
     * given parameters, whose name is {@value #CONTEXT_NAME}.
     *
     * <p>The parameter must exist, that is, it should be a mandatory parameter, otherwise a runtime
     * exception is thrown.
     *
     * @param params the parameters that contain the context's name
     * @return the {@code Context} with the given name
     * @throws ApiException If the context with the given name does not exist
     * @see JSONObject#getString(String)
     */
    private Context getContext(JSONObject params) throws ApiException {
        return ApiUtils.getContextByName(params, CONTEXT_NAME);
    }

    /**
     * Builds the response describing an Context.
     *
     * @param c the context
     * @return the api response
     */
    private ApiResponse buildResponseFromContext(Context c) {
        Map<String, Object> fields = new HashMap<>();
        fields.put("name", c.getName());
        fields.put("id", Integer.toString(c.getId()));
        fields.put("description", c.getDescription());
        fields.put("inScope", Boolean.toString(c.isInScope()));
        fields.put("excludeRegexs", jsonEncodeList(c.getExcludeFromContextRegexs()));
        fields.put("includeRegexs", jsonEncodeList(c.getIncludeInContextRegexs()));
        fields.put(
                "includedTechnologies",
                c.getTechSet().getIncludeTech().stream()
                        .map(Tech::toString)
                        .collect(Collectors.toList()));
        fields.put(
                "excludedTechnologies",
                c.getTechSet().getExcludeTech().stream()
                        .map(Tech::toString)
                        .collect(Collectors.toList()));

        AuthenticationMethod authenticationMethod = c.getAuthenticationMethod();
        if (authenticationMethod != null) {
            Pattern pattern = authenticationMethod.getLoggedInIndicatorPattern();
            fields.put("loggedInPattern", pattern == null ? "" : pattern.toString());
            pattern = authenticationMethod.getLoggedOutIndicatorPattern();
            fields.put("loggedOutPattern", pattern == null ? "" : pattern.toString());
            AuthenticationMethodType type = authenticationMethod.getType();
            fields.put("authType", type == null ? "" : type.getName());

            AuthCheckingStrategy strategy = authenticationMethod.getAuthCheckingStrategy();
            fields.put(PARAM_CHECKING_STRATEGRY, strategy == null ? "" : strategy.name());
            if (AuthCheckingStrategy.POLL_URL.equals(strategy)) {
                fields.put(PARAM_POLL_URL, authenticationMethod.getPollUrl());
                fields.put(PARAM_POLL_DATA, authenticationMethod.getPollData());
                fields.put(PARAM_POLL_HEADERS, authenticationMethod.getPollData());
                fields.put(
                        PARAM_POLL_FREQ, Integer.toString(authenticationMethod.getPollFrequency()));
                AuthPollFrequencyUnits units = authenticationMethod.getPollFrequencyUnits();
                fields.put(PARAM_POLL_FREQ_UNITS, units == null ? "" : units.name());
            }
        }

        AuthorizationDetectionMethod authorizationDetectionMethod =
                c.getAuthorizationDetectionMethod();
        if (authorizationDetectionMethod != null) {
            fields.put(
                    "authenticationDetectionMethodId",
                    String.valueOf(authorizationDetectionMethod.getMethodUniqueIdentifier()));
        }

        fields.put("urlParameterParserClass", c.getUrlParamParser().getClass().getCanonicalName());
        fields.put("urlParameterParserConfig", c.getUrlParamParser().getConfig());
        fields.put(
                "postParameterParserClass", c.getPostParamParser().getClass().getCanonicalName());
        fields.put("postParameterParserConfig", c.getPostParamParser().getConfig());

        return new ContextApiResponseSet<>("context", fields);
    }

    private static class ContextApiResponseSet<T> extends ApiResponseSet<T> {

        ContextApiResponseSet(String name, Map<String, T> values) {
            super(name, values);
        }

        @Override
        public void toXML(Document doc, Element parent) {
            parent.setAttribute("type", "set");
            for (Map.Entry<String, T> val : getValues().entrySet()) {
                Element el = doc.createElement(val.getKey());
                if ("includedTechnologies".equals(val.getKey())
                        || "excludedTechnologies".equals(val.getKey())) {
                    el.setAttribute("type", "list");
                    @SuppressWarnings("unchecked")
                    List<String> techs = (List<String>) val.getValue();
                    for (String tech : techs) {
                        Element elTech = doc.createElement("tech");
                        elTech.appendChild(
                                doc.createTextNode(XMLStringUtil.escapeControlChrs(tech)));

                        el.appendChild(elTech);
                    }
                } else {
                    String textValue = val.getValue() == null ? "" : val.getValue().toString();
                    Text text = doc.createTextNode(XMLStringUtil.escapeControlChrs(textValue));
                    el.appendChild(text);
                }
                parent.appendChild(el);
            }
        }
    }

    private String jsonEncodeList(List<String> list) {
        JSONArray js = new JSONArray();
        for (String item : list) {
            js.add(item);
        }
        return js.toString();
    }

    /**
     * Gets the tech that matches the techName or throws an exception if no tech matches
     *
     * @param techName the name of the tech
     * @return the matching tech
     * @throws ApiException the api exception
     */
    private Tech getTech(String techName) throws ApiException {
        return Optional.ofNullable(Tech.get(techName))
                .orElseThrow(
                        () ->
                                new ApiException(
                                        Type.ILLEGAL_PARAMETER,
                                        "The tech '" + techName + "' does not exist"));
    }
}
